﻿#region Disclaimer/Info
///////////////////////////////////////////////////////////////////////////////////////////////////
// MetaCollection
// 
// MetaCollection is a system to provide a standard structure, permissions and metadata service
// for content organization.
//
// This project is hosted at Google Code at http://code.google.com/p/metacollection/
//
// This project is licensed under the BSD license.  See the License.txt file for more information.
///////////////////////////////////////////////////////////////////////////////////////////////////
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MetaCollection.Framework.Model;
using System.Globalization;

namespace MetaCollection.Framework.Validation
{
	/// <summary>
	/// A validation session contains a copy of the Item Model specification (validation rules)
	/// and the state of the validation in progress.  To perform a validation 
	/// 
	/// Throws exceptions only when trying to validate a field that cannot be validated. That is the exceptional circumstance.
	/// Having invalid values is to be expected due to the nature of the class.
	/// </summary>
	public class ValidationSession
	{
		/// <summary>
		/// list of validated fields.
		/// </summary>
		IDictionary<String, bool?> m_ValidatedFields;
		/// <summary>
		/// list of failures.
		/// </summary>
		List<string> failList;
		/// <summary>
		/// Require a rule to exist for each property that passes through the ValidationSession?
		/// </summary>
		bool m_RequireRuleMatch;
		/// <summary>
		/// Set of validation rules described in IModelProperties
		/// </summary>
		IDictionary<string, IModelProperty> m_ValidationSet;

		/// <summary>
		/// Initializes a new instance of the <see cref="ValidationSession"/> class.
		/// </summary>
		/// <param name="validator">The validator to get the rule specification from.</param>
		internal ValidationSession(ItemValidation validator)
		{
			m_ValidationSet = validator.ItemSpecification.Properties.ToDictionary((p) => p.Name);
			m_RequireRuleMatch = !validator.ItemSpecification.AllowUnknown;

			failList = new List<string>();

			// Has field been validated? It's in here.
			// Was the value of the field valid? It's in here and true.
			// Was the value of the field invalid? It's in here and false.
			m_ValidatedFields = new Dictionary<String, bool?>();
		}

		/// <summary>
		/// Validates that the value given for the key satisfies the validation rules.
		/// </summary>
		/// <param name="key">The key (property name) to validate.</param>
		/// <param name="keyValue">The value to check against the rule(s)</param>
		/// <remarks>A Call to VerifySession should be made to throw an exception if any validate method calls fail.</remarks>
		public void Validate(string key, string value)
		{
			// check to see if this validation set contains a rule for this property.
			if (m_ValidationSet.Keys.Any(k => k == key))
			{
				IModelProperty validationProperty = m_ValidationSet[key];

				// check to see if we have validated this before, if not add an undetermined result.
				if (!m_ValidatedFields.Keys.Any(k => k == key))
					m_ValidatedFields.Add(key, null);

				if (validationProperty.Required && string.IsNullOrEmpty(value))
						m_ValidatedFields[key] = false;
				else
					m_ValidatedFields[key] = ((validationProperty.Validation == null) || (validationProperty.Validation.IsMatch(value)));

				// if a set of valid values where given then check to see that given value is one of them
				if (!string.IsNullOrEmpty(validationProperty.ValidValues))
					m_ValidatedFields[key] = validationProperty.ValidValues.Split(',').Contains(value, StringComparer.OrdinalIgnoreCase);
			}
			else if (m_RequireRuleMatch)
			{
				failList.Add(String.Format(CultureInfo.CurrentCulture, "Property '{0}' is not part of this item specification and unknown properties are disabled.", key));
			}
		}

		/// <summary>
		/// Associates the given assertion with the value given in key.  If the asserion fails then the failmessage is added to 
		/// the list of failed vaildated fields with key as the name of the field.
		/// </summary>
		/// <param name="key">The name of the field or property to associate this assertion with</param>
		/// <param name="assertion">The assertion which returns true is it passes</param>
		/// <param name="failMessage">The fail message to include in the validation report if the assertion fails.</param>
		/// <remarks>A Call to VerifySession should be made to throw an exception if any validate method calls fail.</remarks>
		public void Validate(string key, Func<bool> assertion, string failMessage)
		{
			if (!assertion())
				failList.Add(String.Format(CultureInfo.CurrentCulture, "{0} was not validated. {1}", key, failMessage));
		}
		/// <summary>
		/// Throws an invalid operation exception of any of the Validate() methods called on this session failed.  Will
		/// also throw an exception if a rule in the ItemValidation ruleset was required to be checked and the associated
		/// validate method with the property name for the rule was not called.
		/// </summary>
		/// <exception cref="InvalidOperationException">Thrown if request is not valid.</exception>
		public void VerifySession()
		{
			// all of the fields are valid?
			foreach (var val in m_ValidatedFields)
			{
				if (!val.Value.GetValueOrDefault(false))
					failList.Add(String.Format(CultureInfo.CurrentCulture, "{0} was invalid. {1}", val.Key, GetCustomFailureMessage(val.Key)));
			}

			if (failList.Any())
			{
				string innerMessage = String.Join("\n", failList.ToArray());
				throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Validation Failed: {0} errors occured during validation.\n{1}", failList.Count(), innerMessage));
			}
		}

		/// <summary>
		/// Gets the custom failure message.
		/// </summary>
		/// <param name="key">The key.</param>
		/// <returns>a string version of the failure message for the given key (property or field name associated with a rule)</returns>
		private string GetCustomFailureMessage(string key)
		{
			string customMessage = m_ValidationSet.Keys.Any(k => k == key) ? m_ValidationSet.FirstOrDefault(i => i.Key == key).Value.ErrorMessage : String.Empty;
			return customMessage;
		}

		/// <summary>
		/// Gets the validated fields.
		/// </summary>
		/// <value>The validated fields.</value>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification="We dont want decendants to bind to the ReadOnlyDictionary so we use IDictionary.  It is unfortunate that bool? is considered generic.")]
		public IDictionary<String, bool?> ValidatedFields
		{
			get { return new Helpers.ReadOnlyDictionary<String, Boolean?>(m_ValidatedFields); }
		}

	}

}
